package net.ericaro.diezel.plugin;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;

import net.ericaro.diezel.Diezel;
import net.ericaro.diezel.DiezelException;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;

import edu.uci.ics.jung.graph.Graph;

/** generate a Diezel Mojo
 * 
 * 
 * @author eric
 * @goal diezel
 * @phase generate-sources
 * @since 2.0
 */
public class DiezelMojo extends AbstractMojo {

	/**
	 * The current Maven project.
	 * 
	 * @parameter default-value="${project}"
	 * @readonly
	 * @required
	 */
	private MavenProject project;

	/**
	 * The set of compile source roots whose contents are not generated as part of the build, i.e. those that usually
	 * reside somewhere below "${basedir}/src" in the project structure. Files in these source roots are owned by the
	 * user and must not be overwritten with generated files.
	 */
	private Collection nonGeneratedSourceRoots;

	 /**
     * The directory where the Diezel files (<code>*.dzl</code>) are located.
     * 
     * @parameter expression="${sourceDirectory}" default-value="${basedir}/src/main/diezel"
     */
    private File sourceDirectory;

    /**
     * The directory where the diezel files generated by Diezel will be stored. The directory will be registered as a
     * compile source root of the project such that the generated files will participate in later build phases like
     * compiling and packaging.
     * 
     * @parameter expression="${outputDirectory}" default-value="${project.build.directory}/generated-sources/diezel"
     */
    private File outputDirectory;

    /**
     * The granularity in milliseconds of the last modification date for testing whether a source needs recompilation.
     * 
     * @parameter expression="${lastModGranularityMs}" default-value="0"
     */
    private int staleMillis;
    
    /**
     * Verbose mode
     * 
     * @parameter expression="${debug}" default-value="false"
     */
    private boolean debug;
    

    /**
     * A set of Ant-like inclusion patterns used to select files from the source directory for processing. By default,
     * the patterns <code>**&#47;*.xml</code> and <code>**&#47;*.dzl</code> are used to select Diezel files.
     * 
     * @parameter
     */
    private String[] includes;

    /**
     * A set of Ant-like exclusion patterns used to prevent certain files from being processed. By default, this set is
     * empty such that no files are excluded.
     * 
     * @parameter
     */
    private String[] excludes;

    /**
     * {@inheritDoc}
     */
    protected File getSourceDirectory()
    {
        return this.sourceDirectory;
    }

    /**
     * {@inheritDoc}
     */
    protected String[] getIncludes()
    {
        if ( this.includes != null )
        {
            return this.includes;
        }
        else
        {
            return new String[] { "**/*.xml", "**/*.dzl" };
        }
    }

    /**
     * {@inheritDoc}
     */
    protected String[] getExcludes()
    {
        return this.excludes;
    }

    /**
     * {@inheritDoc}
     */
    protected File getOutputDirectory()
    {
        return this.outputDirectory;
    }

    /**
     * {@inheritDoc}
     */
    protected int getStaleMillis()
    {
        return this.staleMillis;
    }

    /**
     * {@inheritDoc}
     */
    protected File[] getCompileSourceRoots()
    {
        return new File[] { getOutputDirectory() };
    }

	
	/**
	 * Execute the tool.
	 * 
	 * @throws MojoExecutionException
	 *             If the invocation of the tool failed.
	 * @throws MojoFailureException
	 *             If the tool reported a non-zero exit code.
	 */
	public void execute() throws MojoExecutionException, MojoFailureException {
		
		
		File[] diezels = scanForDiezels();
		getLog().info("starting Diezel compilation from: " + getSourceDirectory());

		if (diezels == null) {
			getLog().info("Skipping non-existing diezels in directory: " + getSourceDirectory());
			return;
		} else if (diezels.length <= 0) {
			getLog().info("Skipping - all diezels are up to date");
		} else {
			determineNonGeneratedSourceRoots();
			processDiezel(diezels);
			getLog().info("Processed " + diezels.length + " diezel" + (diezels.length != 1 ? "s" : ""));
		}

		File[] compileSourceRoots = getCompileSourceRoots();
		for (int i = 0; i < compileSourceRoots.length; i++) 
			addSourceRoot(compileSourceRoots[i]);
		
	}

	/**
	 * Passes the specified grammar file through the tool.
	 * 
	 * @param diezel
	 *            The file containing the diezel file to process, must not be <code>null</code>.
	 * @throws MojoExecutionException
	 *             If the invocation of the tool failed.
	 * @throws MojoFailureException
	 *             If the tool reported a non-zero exit code.
	 */
	protected void processDiezel(File[] diezel) throws MojoExecutionException, MojoFailureException{
			try {
				//TODO also copy source definition (.xml for languages), into classes, so that it will be possible 
				// to implement a language defined in another jar.
				//TODO after the previous one : also scan the dependencies for any Diezel definitions.
				Diezel.generate(outputDirectory, diezel);
			} catch (DiezelException e) {
				throw new MojoFailureException(e.getMessage());
			}
	}

	/**
	 * Scans the configured source directory for diezel files which need processing.
	 * 
	 * @return An array of files containing the found grammar files or <code>null</code> if the source
	 *         directory does not exist.
	 * @throws MojoExecutionException
	 *             If the source directory could not be scanned.
	 */
	private File[] scanForDiezels() throws MojoExecutionException {
		if (!getSourceDirectory().isDirectory()) {
			return null;
		}

		getLog().debug("Scanning for grammars: " + getSourceDirectory());
		try {
			DiezelDirectoryScanner scanner = new DiezelDirectoryScanner();
			scanner.setSourceDirectory(getSourceDirectory());
			scanner.setIncludes(getIncludes());
			scanner.setExcludes(getExcludes());
			scanner.setOutputDirectory(getOutputDirectory());
			scanner.setStaleMillis(getStaleMillis());
			scanner.scan();
			File[] diezels = scanner.getIncludedDiezels();
			getLog().debug("Found diezels: " + Arrays.asList(diezels));
			return diezels;

		} catch (Exception e) {
			throw new MojoExecutionException("Failed to scan for grammars: " + getSourceDirectory(), e);
		}
	}

	/**
	 * Determines those compile source roots of the project that do not reside below the project's build directories.
	 * These compile source roots are assumed to contain hand-crafted sources that must not be overwritten with
	 * generated files. In most cases, this is simply "${project.build.sourceDirectory}".
	 * 
	 * @throws MojoExecutionException
	 *             If the compile source rotos could not be determined.
	 */
	private void determineNonGeneratedSourceRoots() throws MojoExecutionException {
		this.nonGeneratedSourceRoots = new LinkedHashSet();
		try {
			String targetPrefix = new File(this.project.getBuild().getDirectory()).getCanonicalPath() + File.separator;
			Collection sourceRoots = this.project.getCompileSourceRoots();
			for (Iterator it = sourceRoots.iterator(); it.hasNext();) {
				File sourceRoot = new File(it.next().toString());
				if (!sourceRoot.isAbsolute()) {
					sourceRoot = new File(this.project.getBasedir(), sourceRoot.getPath());
				}
				String sourcePath = sourceRoot.getCanonicalPath();
				if (!sourcePath.startsWith(targetPrefix)) {
					this.nonGeneratedSourceRoots.add(sourceRoot);
					getLog().debug("Non-generated compile source root: " + sourceRoot);
				} else {
					getLog().debug("Generated compile source root: " + sourceRoot);
				}
			}
		} catch (IOException e) {
			throw new MojoExecutionException("Failed to determine non-generated source roots", e);
		}
	}

	/**
	 * Determines whether the specified directory denotes a compile source root of the current project.
	 * 
	 * @param directory
	 *            The directory to check, must not be <code>null</code>.
	 * @return <code>true</code> if the specified directory is a compile source root of the project, <code>false</code> otherwise.
	 */
	protected boolean isSourceRoot(File directory) {
		return this.nonGeneratedSourceRoots.contains(directory);
	}

	/**
	 * Registers the specified directory as a compile source root for the current project.
	 * 
	 * @param directory
	 *            The absolute path to the source root, must not be <code>null</code>.
	 */
	private void addSourceRoot(File directory) {
		if (this.project != null) {
			getLog().debug("Adding compile source root: " + directory);
			this.project.addCompileSourceRoot(directory.getAbsolutePath());
		}
	}
}
